﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Net;
using System.Net.Sockets;

namespace RVBike
{
    /* Client informations
     * 
     * Name         : User name
     * State        : Client state {NotReady, Ready, Loading, Loaded, Playing}
     * Mileage      : Client mileage after start playing
     * IP           : Client ip
     */
    class RVClientState
    {
        public string Name { get; set; }
        public string State { get; set; }
        public float Mileage { get; set; }
        public string IP { get; set; }
        public string GPS { get; set; }

        public RVClientState(Dictionary<string, string> info)
        {
            Name = info[RVNetworkEnv.USER_NAME];
            State = info[RVNetworkEnv.CLIENT_STATE];
            Mileage = 0.0f; // Default value
            IP = info[RVNetworkEnv.CLIENT_IP];
            GPS = "0,0";
        }
    }

    /* Room informations
     * 
     * RoomName     : Display name in the lobby
     * MapName      : Selected map name
     * Mode         : Game mode
     * CurrentUser  : Current user count in the room
     * MaxUser      : Maximum user count in the room
     * ServerIP     : Server ip
     * MCastIP      : Multicast ip
     * State        : Room state {OPPEN, PLAYING}
     * Clients      : Client list
     */
    class RVRoom
    {
        public string RoomName { get; set; }
        public string MapName { get; set; }
        public string Mode { get; set; }
        public int CurrentUser { get; set; }
        public int MaxUser { get; set; }
        public string ServerIP { get; set; }
        public string MCastIP { get; set; }
        public string State { get; set; }
        public List<RVClientState> Clients { get; set; }
        public bool WaitLoaded { get; set; }

        public RVRoom(Dictionary<string, string> info)
        {
            RoomName = info[RVNetworkEnv.ROOM_NAME];
            MapName = info[RVNetworkEnv.MAP_NAME];
            Mode = info[RVNetworkEnv.GAME_MODE];
            CurrentUser = Int32.Parse(info[RVNetworkEnv.CURRENT_USER_COUNT]);
            MaxUser = Int32.Parse(info[RVNetworkEnv.MAX_USER_COUNT]);
            ServerIP = info[RVNetworkEnv.SERVER_IP];
            MCastIP = info[RVNetworkEnv.MCAST_IP];
            State = info[RVNetworkEnv.ROOM_STATE];

            Clients = new List<RVClientState>();
            if (info.ContainsKey(RVNetworkEnv.CLIENT_LIST))
            {
                string[] clientStrings = info[RVNetworkEnv.CLIENT_LIST].Split('_');
                for (int i = 0; i < clientStrings.Length; i += 3)
                {
                    Dictionary<string, string> clientInfo = new Dictionary<string, string>();
                    clientInfo.Add(RVNetworkEnv.USER_NAME, clientStrings[i]);
                    clientInfo.Add(RVNetworkEnv.CLIENT_STATE, clientStrings[i + 1]);
                    clientInfo.Add(RVNetworkEnv.CLIENT_IP, clientStrings[i + 2]);
                    Clients.Add(new RVClientState(clientInfo));
                }
            }

            WaitLoaded = false;
        }
    }

    /* Game server will host game necessary resources
     *
     * It should respond to following messages :
     *   - JOIN : Client requests to join this room. Reply will be "RT=[OK|FAIL]"
     *   - UPDATE : Client update state to server
     *   - Leave : Client leave this room
     */
    class RVGameServer
    {
        private IPAddress _mcastIP;
        private UdpClient _msgListener;
        private UdpClient _multicaster;
        private Thread _recvThread;

        public RVRoom Room { get; set; }

        public RVNetworkEnv.JoinRoomHandlers JoinRoomHandler { get; set; }
        public RVNetworkEnv.LeaveRoomHandlers LeaveRoomHandler { get; set; }
        public RVNetworkEnv.ClientStateChangeHandlers ClientStateChangeHandler { get; set; }
        public RVNetworkEnv.RoomInfoUpdateHandlers RoomInfoUpdateHandler { get; set; }
        public RVNetworkEnv.ReceiveMsgHandlers ReceiveMsgHandler { get; set; }

        public RVGameServer(RVRoom room, string mip)
        {
            // An empty room
            Room = room;

            _mcastIP = IPAddress.Parse(mip);

            // Connection to recv/send message from/to clients
            _msgListener = new UdpClient();
            _msgListener.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            _msgListener.ExclusiveAddressUse = false;
            _msgListener.Client.Bind(new IPEndPoint(IPAddress.Any, RVNetworkEnv.SERVER_PORT));

            // Connection to send multicast messages
            _multicaster = new UdpClient();
            _multicaster.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            _multicaster.ExclusiveAddressUse = false;

            // [FIXME] : use broadcast to avoid multicast problem in WinXP
            //_multicaster.Client.Bind(new IPEndPoint(IPAddress.Any, RVNetworkEnv.MULTICAST_PORT));
            //_multicaster.JoinMulticastGroup(_mcastIP);
            _multicaster.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);

            _recvThread = new Thread(() => receiveMsg());
            _recvThread.Start();
        }

        public void Close()
        {
            _msgListener.Close();
            _msgListener = null;

            // Broadcast quit message to force client quit
            Dictionary<string, string> quitMsg = new Dictionary<string, string>();
            quitMsg.Add(RVNetworkEnv.COMMAND, RVNetworkEnv.QUIT);
            BroadcastMsg(quitMsg);

            _multicaster.Close();
            _multicaster = null;

            _recvThread.Join();
            _recvThread = null;
        }

        private void receiveMsg()
        {
            if (_msgListener == null) return;

            IPEndPoint iep = new IPEndPoint(IPAddress.Any, RVNetworkEnv.SERVER_PORT);

            try
            {
                while (true)
                {
                    byte[] data = _msgListener.Receive(ref iep);

                    if (data[0] == RVNetworkEnv.PACKET_MESSAGE)
                    {
                        string message = Encoding.UTF8.GetString(data);
                        System.Diagnostics.Trace.WriteLine(String.Format("Receive message : {0}", message));
                        ParseMsg(message);
                    }
                    else if (data[0] == RVNetworkEnv.PACKET_VIDEO)
                    {
                        System.Diagnostics.Trace.WriteLine("Receive video using from client");
                        ParseAVMsg(data);
                    }
                    else if (data[0] == RVNetworkEnv.PACKET_AUDIO)
                    {
                        System.Diagnostics.Trace.WriteLine("Receive audio from client");
                        ParseAVMsg(data);
                    }
                    else
                    {
                        System.Diagnostics.Trace.WriteLine("Receive unknown packet from client");
                    }
                }
            }
            catch (System.Net.Sockets.SocketException)
            {
                System.Diagnostics.Trace.WriteLine("Socket closed");
            }
        }

        private void Reply(Dictionary<string, string> msg, bool result)
        {
            StringBuilder sb = new StringBuilder();

            // Add packet type
            sb.Append(RVNetworkEnv.PACKET_MESSAGE).Append("?");
            sb.Append("CMD=RP&RT=").Append(result ? "OK" : "FAIL");

            IPEndPoint ep = new IPEndPoint(IPAddress.Parse(msg[RVNetworkEnv.CLIENT_IP]), RVNetworkEnv.CLIENT_PORT);
            byte[] data = Encoding.UTF8.GetBytes(sb.ToString());

            _msgListener.Send(data, data.Length, ep);
        }

        private void Reply(Dictionary<string, string> msg, string ip)
        {
            StringBuilder sb = new StringBuilder();

            // Add packet type
            sb.Append(RVNetworkEnv.PACKET_MESSAGE).Append("?");

            bool isFirst = true;
            foreach (KeyValuePair<string, string> kvp in msg)
            {
                if (!isFirst) sb.Append("&");
                sb.Append(kvp.Key).Append("=").Append(kvp.Value);
                isFirst = false;
            }

            IPEndPoint ep = new IPEndPoint(IPAddress.Parse(ip), RVNetworkEnv.CLIENT_PORT);
            byte[] data = Encoding.UTF8.GetBytes(sb.ToString());

            _msgListener.Send(data, data.Length, ep);
        }

        private void ParseAVMsg(byte[] data)
        {
            Dictionary<string, string> info = new Dictionary<string, string>();

            char packetType = (char)data[0];

            // get command
            int commandStartIdx = 2;
            int commandEndInx = commandStartIdx;
            for (; (char)data[commandEndInx] != '&'; ++commandEndInx) ;
            string[] commandStrings = Encoding.UTF8.GetString(data, commandStartIdx, commandEndInx - commandStartIdx).Split('=');
            info.Add(RVNetworkEnv.COMMAND, commandStrings[1]);

            // get ip
            int ipStartIdx = commandEndInx + 1;
            int ipEndIdx = ipStartIdx;
            for (; (char)data[ipEndIdx] != '&'; ++ipEndIdx) ;
            string[] ipStrings = Encoding.UTF8.GetString(data, ipStartIdx, ipEndIdx - ipStartIdx).Split('=');
            info.Add(RVNetworkEnv.CLIENT_IP, ipStrings[1]);

            int payloadStartIdx = ipEndIdx + 1;
            int payloadSize = data.Length - payloadStartIdx;

            Reply(info, true);

            // Audio/Video packets are always multicast in the lan
            BroadcastMsg(data);
        }

        private void ParseMsg(string message)
        {
            // Convert string to key-value parameter pairs
            Dictionary<string, string> info = new Dictionary<string, string>();
            string[] parameters = message.Substring(2).Split('&');  // skip packet type : "M?"
            foreach (string s in parameters)
            {
                string[] keyVal = s.Split('=');
                info.Add(keyVal[0], keyVal[1]);
            }

            string command;
            if (!info.TryGetValue(RVNetworkEnv.COMMAND, out command))
            {
                // No command parameter ... invalid message
                return;
            }

            try
            {
                System.Diagnostics.Trace.WriteLine(String.Format("Receive command = {0}", command));

                // Callback
                if (ReceiveMsgHandler != null) ReceiveMsgHandler(info);

                switch (command)
                {
                    case RVNetworkEnv.JOIN_ROOM:
                        {
                            // Reject if full
                            if (Room.CurrentUser >= Room.MaxUser)
                            {
                                Reply(info, false);
                            }
                            else
                            {
                                Room.Clients.Add(new RVClientState(info));
                                Room.CurrentUser++;

                                if (JoinRoomHandler != null)
                                {
                                    JoinRoomHandler(info[RVNetworkEnv.USER_NAME], info[RVNetworkEnv.CLIENT_IP]);
                                }

                                Reply(info, true);

                                BroadcastMsg(info);
                            }

                            break;
                        }

                    case RVNetworkEnv.LEAVE_ROOM:
                        {
                            int index = 0;
                            for (; index < Room.Clients.Count; ++index)
                            {
                                RVClientState cl = Room.Clients[index];
                                if (cl.IP == info[RVNetworkEnv.CLIENT_IP]) break;
                            }

                            // Client not found
                            if (index == Room.Clients.Count)
                            {
                                Reply(info, false);
                            }
                            else
                            {
                                Room.CurrentUser--;
                                Room.Clients.RemoveAt(index);

                                if (LeaveRoomHandler != null)
                                {
                                    LeaveRoomHandler(info[RVNetworkEnv.CLIENT_IP]);
                                }

                                Reply(info, true);

                                BroadcastMsg(info);
                            }
                            break;
                        }

                    case RVNetworkEnv.PING:
                        {
                            // A testing purpose message, always reply "OK"
                            Reply(info, true);
                            break;
                        }

                    case RVNetworkEnv.QUERY_ROOM_INFO:
                        {
                            Dictionary<string, string> roomInfo = new Dictionary<string, string>();
                            roomInfo.Add(RVNetworkEnv.COMMAND, RVNetworkEnv.ROOM_INFO);
                            roomInfo.Add(RVNetworkEnv.ROOM_NAME, Room.RoomName);
                            roomInfo.Add(RVNetworkEnv.MAP_NAME, Room.MapName);
                            roomInfo.Add(RVNetworkEnv.GAME_MODE, Room.Mode);
                            roomInfo.Add(RVNetworkEnv.CURRENT_USER_COUNT, Room.CurrentUser.ToString());
                            roomInfo.Add(RVNetworkEnv.MAX_USER_COUNT, Room.MaxUser.ToString());
                            roomInfo.Add(RVNetworkEnv.SERVER_IP, NetworkUtility.MyIP());
                            roomInfo.Add(RVNetworkEnv.ROOM_STATE, Room.State);
                            roomInfo.Add(RVNetworkEnv.MCAST_IP, _mcastIP.ToString());

                            // compose client state string
                            if (Room.Clients.Count > 0)
                            {
                                bool isFirst = true;
                                StringBuilder sb = new StringBuilder();
                                foreach (RVClientState cs in Room.Clients)
                                {
                                    if (!isFirst) sb.Append("_");
                                    sb.Append(cs.Name);
                                    sb.Append("_").Append(cs.State);
                                    sb.Append("_").Append(cs.IP);

                                    isFirst = false;
                                }
                                roomInfo.Add(RVNetworkEnv.CLIENT_LIST, sb.ToString());
                            }

                            Reply(roomInfo, info[RVNetworkEnv.CLIENT_IP]);
                            break;
                        }

                    case RVNetworkEnv.UPDATE_STATE:
                        {
                            int index = 0;
                            for (; index < Room.Clients.Count; ++index)
                            {
                                RVClientState cl = Room.Clients[index];
                                if (cl.IP == info[RVNetworkEnv.CLIENT_IP]) break;
                            }

                            // Client not found
                            if (index == Room.Clients.Count)
                            {
                                Reply(info, false);
                            }
                            else
                            {
                                RVClientState cl = Room.Clients[index];
                                cl.State = info[RVNetworkEnv.CLIENT_STATE];
                                cl.Mileage = float.Parse(info[RVNetworkEnv.MILEAGE]);
                                cl.GPS = info[RVNetworkEnv.GPS];

                                if (ClientStateChangeHandler != null)
                                {
                                    ClientStateChangeHandler(
                                        info[RVNetworkEnv.CLIENT_IP], 
                                        float.Parse(info[RVNetworkEnv.MILEAGE]), 
                                        info[RVNetworkEnv.CLIENT_STATE],
                                        info[RVNetworkEnv.GPS]);
                                }

                                Reply(info, true);
                                BroadcastMsg(info);

                                if (Room.WaitLoaded)
                                {
                                    // Check whether all clients are loaded?
                                    bool allLoaded = true;
                                    for (index = 0 ; index < Room.Clients.Count; ++index)
                                    {
                                        RVClientState c2 = Room.Clients[index];
                                        if (c2.State != RVNetworkEnv.CLIENT_STATE_LOADED)
                                        {
                                            allLoaded = false;
                                            break;
                                        }
                                    }

                                    if (allLoaded)
                                    {
                                        Dictionary<string, string> startPlaying = new Dictionary<string, string>();
                                        startPlaying.Add(RVNetworkEnv.COMMAND, RVNetworkEnv.START_PLAY);
                                        BroadcastMsg(startPlaying);
                                    }
                                }
                            }
                            break;
                        }

                    case RVNetworkEnv.GAME_START:
                        {
                            bool allReady = true;

                            for (int index = 0 ; index < Room.Clients.Count; ++index)
                            {
                                RVClientState cl = Room.Clients[index];
                                if (cl.IP == info[RVNetworkEnv.CLIENT_IP]) continue;
                                if (cl.State != RVNetworkEnv.CLIENT_STATE_READY)
                                {
                                    allReady = false;
                                    break;
                                }
                            }

                            // Somebody is not ready
                            if (!allReady)
                            {
                                Reply(info, false);
                            }
                            else
                            {
                                Room.State = RVNetworkEnv.ROOM_STATE_PLAYING;
                                Room.WaitLoaded = true;

                                Reply(info, true);
                                BroadcastMsg(info);
                            }
                            break;
                        }
                }
            }
            catch
            {
                // Something wrong during processing information.
                // Just skip this message!
                Reply(info, false);
            }
        }

        private void BroadcastMsg(byte[] data)
        {
            //IPEndPoint ep = new IPEndPoint(_mcastIP, RVNetworkEnv.MULTICAST_PORT);

            // [FIXME] : use broadcast to avoid WinXP problem
            IPEndPoint ep = new IPEndPoint(IPAddress.Broadcast, RVNetworkEnv.MULTICAST_PORT);
            _multicaster.Send(data, data.Length, ep);
        }

        private void BroadcastMsg(Dictionary<string, string> msg)
        {
            StringBuilder sb = new StringBuilder();

            sb.Append(RVNetworkEnv.PACKET_MESSAGE).Append("?");

            bool isFirst = true;
            foreach (KeyValuePair<string, string> kvp in msg)
            {
                if (!isFirst) sb.Append("&");
                sb.Append(kvp.Key).Append("=").Append(kvp.Value);
                isFirst = false;
            }

            //IPEndPoint ep = new IPEndPoint(_mcastIP, RVNetworkEnv.MULTICAST_PORT);

            // [FIXME] : use broadcast to avoid WinXP problem
            IPEndPoint ep = new IPEndPoint(IPAddress.Broadcast, RVNetworkEnv.MULTICAST_PORT);
            byte[] data = Encoding.UTF8.GetBytes(sb.ToString());

            _multicaster.Send(data, data.Length, ep);
        }
    }
}

